#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>

// 下面这四个头文件是socket编程中必须包含的，经常使用
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

// 打印程序使用说明的函数
// proc: 程序名称
void Usage(std::string proc)
{
    // 输出使用说明，告知用户程序的使用方式
    std::cout << "Usage:\n\t" << proc << " serverip serverport\n"
              << std::endl;
}

// ./udpclient serverip serverport
int main(int argc, char *argv[])
{
    // 检查命令行参数数量是否为3（程序名、服务器IP、服务器端口）
    if (argc != 3)
    {
        // 参数数量不对，调用Usage函数打印使用说明
        Usage(argv[0]);
        // 退出程序，返回错误码1
        exit(1);
    }
    // 从命令行参数中获取服务器的IP地址
    std::string serverip = argv[1];
    // 从命令行参数中获取服务器的端口号，并将其转换为uint16_t类型
    uint16_t serverport = std::stoi(argv[2]);

    // 1. 创建socket
    // 使用socket函数创建一个UDP套接字，AF_INET表示使用IPv4地址族，SOCK_DGRAM表示使用UDP协议
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    // 检查socket是否创建成功
    if (sockfd < 0)
    {
        // 若创建失败，输出错误信息
        std::cerr << "socket error" << std::endl;
    }
    // 2. client要不要bind？一定要，client也要有自己的IP和PORT。要不要显式[和server一样用bind函数]的bind？不能！不建议！！
    // a. 如何bind呢？udp client首次发送数据的时候，OS会自己自动随机的给client进行bind --- 为什么？防止client port冲突。要bind，必然要和port关联！
    // b. 什么时候bind呢？首次发送数据的时候

    // 构建目标主机的socket信息
    // 定义一个sockaddr_in结构体变量server，用于存储服务器的地址信息
    struct sockaddr_in server;
    // 将server结构体的内存清零，确保没有残留数据
    memset(&server, 0, sizeof(server));
    // 设置地址族为IPv4
    server.sin_family = AF_INET;
    // 将服务器端口号从主机字节序转换为网络字节序
    server.sin_port = htons(serverport);
    // 将服务器的IP地址从点分十进制字符串转换为网络字节序的二进制形式
    server.sin_addr.s_addr = inet_addr(serverip.c_str());

    // 定义一个字符串变量message，用于存储用户输入的消息
    std::string message;
    // 2. 直接通信即可
    // 进入一个无限循环，持续与服务器进行通信
    while (true)
    {
        // 提示用户输入消息
        std::cout << "Please Enter# ";
        // 从标准输入读取一行用户输入的消息
        std::getline(std::cin, message);
        // 使用sendto函数将消息发送给服务器
        // sockfd: 套接字描述符
        // message.c_str(): 消息的字符串指针
        // message.size(): 消息的长度
        // 0: 标志位，一般设为0
        // (struct sockaddr*)&server: 服务器的地址信息
        // sizeof(server): 服务器地址信息的长度
        sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&server, sizeof(server));

        // 定义一个sockaddr_in结构体变量peer，用于存储发送方（服务器）的地址信息
        struct sockaddr_in peer;
        // 定义一个socklen_t类型的变量len，用于存储地址信息的长度
        socklen_t len = sizeof(peer);
        // 定义一个字符数组buffer，用于存储从服务器接收的数据
        char buffer[1024];
        // 使用recvfrom函数从服务器接收数据
        // sockfd: 套接字描述符
        // buffer: 存储接收数据的缓冲区
        // sizeof(buffer)-1: 缓冲区的大小，减1是为了给字符串结束符留位置
        // 0: 标志位，一般设为0
        // 因为客户端不止向一个服务器发送数据，所以需要定义一个sockaddr_in结构体变量peer，用于存储发送方的地址信息
        // (struct sockaddr*)&peer: 发送方的地址信息
        // &len: 地址信息长度的指针
        ssize_t n = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
        // 检查是否成功接收到数据
        if (n > 0)
        {
            // 在接收到的数据末尾添加字符串结束符
            buffer[n] = 0;
            // 输出服务器的响应消息
            std::cout << "server echo# " << buffer << std::endl;
        }
    }
    return 0;
}